Corvina application
A Corvina application is a software that is not part of Corvina codebase but that can show something inside Corvina as if it were part of our platform. Every web application can be a Corvina application!
You can think an external application as a micro-frontend of Corvina that follows the backend for frontend pattern (more details about these definitions here, the Martin Fowler website).
We compose our micro-fronted using iframes in that way:
We'll cover the technical aspect in the next sections.
A Corvina application can be added to the Corvina app store and then installed by a Corvina user in its own organization.
You can host your application in any private or public cloud solution. Your application === your hosting.
Manifest.json
Each Corvina application is described by a manifest.json file that contains some information useful for the integration.
{
// unique identifier of the application
"key": "corvina-app-nodered",
// name of the application in the app store
"name": "NodeRED Cloud Service",
// description of the application in the app store
"description": "Create NodeRED instances managed by Corvina in the cloud",
// status of the application in the app store, can be ACTIVE or UNDER_EVALUATION.
// Apps with UNDER_EVALUATION status cannot be installed, those apps are used for preview purpose.
"status": "ACTIVE",
// images of the application in the app store carousel
"images": [
{
"url": "/static/flow-1.webp",
"thumbnailUrl": "/static/flow-1.thumbnail.webp"
},
{
"url": "/static/flow-2.png",
"thumbnailUrl": "/static/flow-2.thumbnail.png"
}
],
// image of the application in the app store list
"coverImageUrl": "/static/cover.png",
// url of the application in all the following relative urls
"baseUrl": "https://nodered.corvina.cloud/v1",
// (feature preview) If true, we enable the device to call our services calling key.device.corvina.io
// (or key.device.corvina.cloud) using its own certificate
"enableDeviceAccess": false,
// version of the manifest.json file, we use it to check if the application has been updated.
// You must use semantic versioning (https://semver.org/)
"apiVersion": "1.0.21",
// authentication type of the application
"authentication": {
// all our application tokens are JWT tokens (see https://jwt.io/)
"type": "JWT"
},
// The usage of this app require a payment? If so, set free to false. Default true.
"free": true,
// vendor of the application, used in the app store details page
"vendor": {
"name": "Corvina",
"website": "http://www.corvina.io",
// email of the vendor, used in the app store details page (optional).
// This email can be used by Corvina users to contact you for support or further information.
// This email can also be used by Corvina to send you notifications about API deprecations, security issues, scheduled maintenance, etc.
"email": "info@corvina.io"
},
"links": {
// tell to Corvina where to find the manifest.json file (if not provided, Corvina will concatenate the baseUrl with /manifest.json).
// We will poll on this url to check if the application has been updated.
"self": "/manifest.json",
// You can provide a changelog reference that will be shown in the app store details page (optional)
"changelog": "/changelog.html",
},
"lifecycle": {
// Corvina will call this url when the application is installed
"installed": "/installed",
// Corvina will call this url when the application is uninstalled
"uninstalled": "/uninstalled",
// Corvina will call this url when the application is upgraded (optional)
"upgradeOk": "/upgradeOk",
// Corvina will call this url when the application upgrade fails (optional)
"upgradeKo": "/upgradeKo"
},
// In case your application needs other Corvina applications to works, you have to list needed application keys (optional)
"dependsOn": ["corvina-app-artifact-registry"],
"hooks": {
// information used after installation to create a menu entry in the Corvina app
"globalPage": {
// unique identifier of the menu entry
"id": "corvina-app-nodered-globalPage",
// name of the menu entry
"title": "NodeRED Cloud Service",
// url of the menu entry, it's the first url called when the user click on the menu entry and we use it to load the iframe
"url": "https://nodered.corvina.cloud/#/",
// icon of the menu entry
"iconUrl": "/static/icon.svg",
// default false, if true Corvina will not add query params to the url
"avoidCorvinaQueryParams": true
},
// each item in the array will create a menu entry in the Corvina app navigation drawer
"navigationDrawerPages": [
{
// name of the menu entry
"title": {
"value": "NodeRED Instances",
// optional, if not present the value is used
"i18n": "navigationDrawerPage.instances.title"
},
// url of the menu entry, it's the first url called when the user click on the menu entry and we use it to load the iframe
"url": "https://nodered.corvina.cloud/#/instances",
// svg icon of the menu entry
"iconUrl": "/static/icon.svg",
// default false, if true Corvina will not add query params to the url
"avoidCorvinaQueryParams": true
}
]
},
// some value identified by i18n keys need to be translated. This section declare where the translation files are located.
"translations": {
// the key is the language code underscore the region code, the value is the url of the translation file.
// language code follows the ISO 639-1 standard, region code follows the ISO 3166-1 alpha-2 standard.
"urls": {
"en_US": "/i18n/en_US.json",
"it_IT": "/i18n/it_IT.json"
}
},
// scopes of the application, used to define the permissions of the application in terms of application role or device role.
// Corvina will creates a service account for the application and will assign the scopes to the service account.
"scopes": {
"applications": [
"iam.devices.read",
"iam.models.read"
],
"devices": [
{
"deviceGroups": [
"*"
],
"generalPermission": "REGULAR_USER",
"modelPermissions": [
"**"
]
}
]
},
// addtional roles to add at installation time
"additionalRoles": [
{ "name": "readonly", "description": "Readonly role for Nodered app" }
]
}
Each relative url in the manifest.json file is relative to the baseUrl
property, but you can use absolute urls. For example, if the baseUrl
is https://nodered.corvina.cloud/v1
and the lifecycle.installed
is /installed
, the url called by Corvina will be https://nodered.corvina.cloud/v1/installed
. If the lifecycle.installed
is https://nodered.corvina.cloud/v2/anotherInstallation
, the url called by Corvina will be https://nodered.corvina.cloud/v2/anotherInstallation
regardless of the baseUrl
property.
All the urls in the manifest.json
file are called by Corvina, so they must be accessible from the internet. You have to take care of the content of the response of each url.
In you use dependsOn
field, you have to install needed listed applications, before to install yours. On frontend side the installation button will be disabled if needed listed applications are not installed yet.
It's very important to understand that Corvina will create a service account for the application and will assign the scopes to the service account. The service account, is identified by a couple of strings:
- client id
- client secret
that are used to authenticate the application to Corvina. Corvina will pass those information to the external app during the installation API call.
Installation
When a user installs an application, Corvina will call the installation API of the application. The installation API is defined in the manifest.json file, Corvina will performa a POST request to the url defined in the lifecycle.installed
property. The payload is a json object:
{
// unique identifier of the application, as defined in the manifest.json file
"key": "corvina-app-nodered",
// version of the manifest.json file
"apiVersion": "1.0.21",
// client id of the service account created for the application
"clientId": "user-service-noderedadmin@myorg",
// client secret of the service account created for the application
"clientSecret": "43024c91-cd1e-4877-bc21-6e61b2d80a49",
// identifier of the organization where the application is installed
"organizationId": 36,
// identifier of the Corvina instance
"instanceId": "c9beedd2-6c15-4c85-bb2f-50ec579c9ae6",
// the string version of the organization identifier
"orgResourceId": "exor.example",
// an event type that can be used to distinguish why this API is called
"eventType": "installed",
// the realm name of the organization where the application is installed
"realm": "myorg",
// only the user with this role can access the application, you can check if the jwt received in the request contains this role. We'll explain this better in the frontend integration section
"realmValidationRole": "monitoring.roles.app_nodered_administrator",
// the platform host of the Corvina instance where the application is installed
"baseUrl": "https://app.corvina.io",
// the websocket url of the Corvina instance where the application is installed
"wsBaseUrl": "wss://app.corvina.io",
// the API url of the Corvina instance where the application is installed
"apiBaseUrl": "https://app.corvina.io",
// the host of our authentication server
"authBaseUrl": "https://auth.corvina.io",
// this is the OpenId connect discovery endpoint, you can use it to retrieve the public key of the jwt token in order to verify the Corvina JWT signature.
"openIdConfigurationUrl": "https://auth.corvina.io/auth/realms/sample1/.well-known/openid-configuration",
// In case of dependsOn in manifest.json, you will receive dependencies' manifests to get needed information.
// In dependencies you receive a JSON that has application key as key and application manifest as value
"dependencies": {
"corvina-app-artifact-registry": {
"key": "corvina-app-artifact-registry",
"name": ...
}
}
}
This is a webhook, so we follow our Webhook guidelines.
If the installation succeeds, the user will see that the application is in status INSTALLED and will be able to access it. If the installation does not succeed, the user will see that the application is not installed and will be able to retry the installation.
The following is an example of a successful installation:
Uninstallation
When a user uninstalls an application, Corvina will call the uninstallation API of the application. The uninstallation API is defined in the manifest.json
file, Corvina will performa a POST request to the url defined in the lifecycle.uninstalled
property. The payload is a json object:
{
// unique identifier of the application, as defined in the manifest.json file
"key": "corvina-app-nodered",
// version of the manifest.json file
"apiVersion": "1.0.21",
// identifier of the organization where the application is installed
"organizationId": 36,
// identifier of the Corvina instance
"instanceId": "c9beedd2-6c15-4c85-bb2f-50ec579c9ae6",
// an event type that can be used to distinguish why this API is called
"eventType": "uninstalled",
// the platform host of the Corvina instance where the application is installed
"baseUrl": "https://app.corvina.io",
// the API url of the Corvina instance where the application is installed
"apiBaseUrl": "https://app.corvina.io",
// the host of our authentication server
"authBaseUrl": "https://auth.corvina.io",
// this is the OpenId connect discovery endpoint, you can use it to retrieve the public key of the jwt token in order to verify the Corvina JWT signature.
"openIdConfigurationUrl": "https://auth.corvina.io/auth/realms/sample1/.well-known/openid-configuration"
}
This is a webhook, so we follow our Webhook guidelines.
In this example, the application is uninstalled from the organization with id 36 and instance id c9beedd2-6c15-4c85-bb2f-50ec579c9ae6
(if you created some resources in the installation API, you can delete them in the uninstallation API).
If the uninstallation does not succeed, the user will see that the application is in status "UNINSTALLATION_FAILED" and will be able to retry the uninstallation indefinitely.
Frontend integration
Your application will be integrated in the Corvina frontend via iframe using the hooks.globalPage.url
property defined in the manifest.json file. The url defined in the hooks.globalPage.url
property will be called with the following query parameters:
accessToken
: the jwt token of the user that is using your applicationinstanceId
: the id of the Corvina instance where the application is installedorganizationId
: the id of the organization where the application is installedcorvinaHost
: the url encoded baseUrl of the Corvina instance where the application is installedlocale
: the locale of the user that is using your application, in the formatlanguage_country
apiVersion
: the user's is trying to access your application attempting to use this API version (see Application upgrade for more details)
The resulting url will be something like this:
https://nodered.corvina.cloud/#/?apiVersion=1.0.21&locale=it_IT&instanceId=c9beedd2-6c15-4c85-bb2f-50ec579c9ae6&organizationId=36&corvinaHost=https%3A%2F%2Fapp.corvina.io&accessToken=...
It's important to notice that if you specify avoidCorvinaQueryParams
the resulting url will be:
https://node-red.corvina.cloud/#/
Use the javascript function decodeURIComponent
to decode the corvinaHost
parameter.
const corvinaHost = decodeURIComponent('https%3A%2F%2Fapp.corvina.io');
console.log(corvinaHost); // https://app.corvina.io
If the user change the organization, the organizationId
parameter will be updated.
The smartest way to integrate your application with Corvina is using our frontend library called corvina-app-connect. It's written in typescript to give you the best developer experience.
You can use it installing via npm or referencing it in your html page!
This library will help you to:
- retrieve the jwt token of the user that is using your application, so you can use it to authenticate your backend (remember to retrieve the public key from the
openIdConfigurationUrl
from the installation API payload) - get notified when the jwt token is refreshed
- get notified when the user changes the organization (this is useful if your
hooks.globalPage.url
refers to a Single Page Application)
We develop this library because we want to support server side rendering and client side rendering as well.
Backend authentication
Once you perform the frontend integration, you can use the jwt token to authenticate API calls or websocket connections to your backend. You can call the openIdConfigurationUrl
to retrieve the public key of the jwt token and use it to verify the token (we suggest you to cache result, the rolling of the public key doesn't happen often).
The jwt token content will respect this interface:
interface ICorvinaToken {
// More details here https://www.rfc-editor.org/rfc/rfc7519
// validate it! This string must contains the authBaseUrl from the installation API payload
iss: string,
// More details here https://www.rfc-editor.org/rfc/rfc7519
aud: string,
// More details here https://www.rfc-editor.org/rfc/rfc7519
sub: string,
// More details here https://www.rfc-editor.org/rfc/rfc7519
// validate it!
exp: number,
// More details here https://www.rfc-editor.org/rfc/rfc7519
iat: number,
// More details here https://www.rfc-editor.org/rfc/rfc7519
jti: string,
// More details here https://www.rfc-editor.org/rfc/rfc7519
typ: string,
email?: string,
email_verified: boolean,
preferred_username: string,
realm_access: {
// validate it! It's the role that you can use to check if the user can access your application, one of the roles must match the `realmValidationRole` from the installation API payload
roles: string[],
},
resource_access?: {
[key: string]: {
roles: string[],
},
},
scope?: string,
}
If you want to detect the username of the user that is using your application, you can use the preferred_username
field. If the jwt refers to a service account, the preferred_username
field has the following format:
service-account-user-service-<name>@<organization-host-name>
You can remove the service-account-user-service-
prefix to get the name of the service account.
For complex applications with complex role system, the jwt token size can be greater than 4kb. In this case, we suggest you to increase the maxHttpHeaderSize
of your http server.
Backend integration
If the backend of your application needs to communicate with Corvina, you can use the clientId
and clientSecret
from the installation API payload to authenticate your application to Corvina. You can use the apiBaseUrl
from the installation API payload to call the Corvina API.
Example of login with clientId and clientSecret:
curl -X POST https://auth.corvina.io/auth/realms/myorg/protocol/openid-connect/token \
-H 'Authorization: Basic dXNlci1zZXJ2aWNlLXRlc3RpbmdAZXhvcjozODUzY2IyNS1kY2E4LTQwMGItYWE5Ni1iNDllYzUyNjExOTg=' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'grant_type=client_credentials&scope=org:exor.example'
If you are not familiar with basic authentication, you can use the
clientId
andclientSecret
to generate the base64 encoded string that you need to pass in theAuthorization
header.
The response will be:
{
"upgraded": false,
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJpSE1zS004Ulo5TFlFTllfMnV0Y2dwcUtBdWMwVXdndGUybFJqbG01NDJNIn0.eyJleHAiOjE2NzQ2Nzg4OTgsImlhdCI6MTY3NDY3ODU5OCwianRpIjoiMDBiODRlNmYtYzYzZS00OTE0LTg2NzQtMTliNmNlNTY5NjdhIiwiaXNzIjoiaHR0cHM6Ly9hdXRoLmNvcnZpbmEuZm9nOjEwNDQzL2F1dGgvcmVhbG1zL2V4b3IiLCJhdWQiOiJjb3J2aW5hLXBsYXRmb3JtIiwic3ViIjoiMGEyZmFhY2ItNWNmYS00N2U1LWFjYTctMzk5ZDIzMjQzYTVkIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoidXNlci1zZXJ2aWNlLXRlc3RpbmdAZXhvciIsImFjciI6IjEiLCJhbGxvd2VkLW9yaWdpbnMiOlsiKiJdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiZGVmYXVsdC1yb2xlcy1leG9yIiwib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiIsImV4b3Iucm9sZXMuYXBwX2FkbWluaXN0cmF0b3IiXX0sInJlc291cmNlX2FjY2VzcyI6eyJ1c2VyLXNlcnZpY2UtdGVzdGluZ0BleG9yIjp7InJvbGVzIjpbInVtYV9wcm90ZWN0aW9uIl19LCJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJhdXRob3JpemF0aW9uIjp7InBlcm1pc3Npb25zIjpbeyJzY29wZXMiOlsiaWFtLm9yZ2FuaXphdGlvbnMuKiIsImlhbS5kZXZpY2VzLioiLCJpYW0ucG9ydGFsLioiLCJpYW0udXNlcmdyb3Vwcy4qIiwiaWFtLmRldmljZWdyb3Vwcy4qIiwiaWFtLnByb2R1Y3RzLioiLCJpYW0ucm9sZXMuKiIsImlhbS5hdWRpdC4qIiwiaWFtLm1vZGVscy4qIiwiaWFtLm5vdGlmaWNhdGlvbnMuKiIsImlhbS51c2Vycy4qIiwiaWFtLmRhc2hib2FyZHMuKiIsImlhbS5hbGFybXMuKiJdLCJyc2lkIjoiOTRhOTI0ZmQtODQyZC00NjA2LWFhZGEtNDQyMzNlYzRiNjMwIiwicnNuYW1lIjoiZXhvciJ9XX0sInNjb3BlIjoiZW1haWwgcHJvZmlsZSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwicHJlZmVycmVkX3VzZXJuYW1lIjoic2VydmljZS1hY2NvdW50LXVzZXItc2VydmljZS10ZXN0aW5nQGV4b3IifQ.XghPVK7E2kwmmWoTbG4hcHF88uDihgTImN0hzZ9biudlDxXvWGY7FtzbX5m8CrP9tbIi4nPrph3Gszaq1D8_bfkBQjLYElATw0crztWUVw9C5Cbf5VManEhrBLco93PtE9TIul6Geox1JzF9ibVhZEIm6gOwkhNnZazbrtJKx_prh40a5sle6MXV1LIrFKkXZuAT6EU2kfpKjNpwvMoPJrHtys9FwHWkCsNXRtPNcddMdWVPPW0InwpSHmKDEVeD45Nzc_RPkErLVkBAWD8_JHix3oWiPcfYGbFwTFuDxmECXEeobU_GR7SdC46Fzc3b8yxxljLVYJEp1AERq6CN8A",
"expires_in": 300,
"refresh_expires_in": 0,
"token_type": "Bearer",
"not-before-policy": 0
}
You can use the access_token to call the Corvina API. Remember to honor the expires_in
value and refresh the token it expires. With the access_token
you have the permissions that you declare in the scopes
parameter of the manifest.json
.
Translations
We want to make our app available to users in languages other than English, so some of the strings in the manifest.json file are translatable.
A common structure for string translatable is:
{
"name": {
"value": "My name",
"i18n": "my-name"
},
}
The value
field is the default value of the string, and the i18n
field is the key of the string in the translation file. The translation files are listed in the translations field of the manifest.json file.
Each translation file is a json file with the following structure:
{
"key1": "translation of key1",
"key2": "translation of key2",
"my-name": "My name in the current language"
}
Limitations:
- the maximum size of a translation file is 32 kB
- a translation string cannot be longer than 1kB
The translation files are downloaded during app installation without any authentication (they must be publicly available). The installation will fail if any of the files are unavailable at that time.
Code sample
You can find a sample application in this repository. It's a simple application made by two pieces:
- a frontend application made with vue.js in the app folder
- a backend application made with node.js, typescript and Nest.js in the service folder
If you want to develop locally, you can run the fake-http-server
that will simulate the Corvina API and the authentication server. You can find the fake-http-server
in the fake-http-server folder
.
In the helm-charts
folder you can find the helm chart to deploy the application in a kubernetes cluster. But this is not mandatory, you can deploy the application in any way you want!
Create Corvina App
We developed a CLI tool to help you to create a Corvina application. You can find it here. Your productivity is important for us, so we want to help you to create a Corvina application in the fastest way possible!
Testing your application
To test your application inside Corvina, there are two ways:
- ask us to install your application in App Store (see Contact us)
- install your application clicking on "Try custom app" in the "Manage" tap of the App Store